package org.apache.lucene.ocean.snapshotlog;

import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.lucene.ocean.LogDirectory;
import org.apache.lucene.ocean.SnapshotInfo;
import org.apache.lucene.ocean.snapshotlog.SnapshotLogFile.InfoHeader;
import org.apache.lucene.ocean.util.CElement;
import org.apache.lucene.ocean.util.SortedListMap;
import org.apache.lucene.ocean.util.XMLUtil;
import org.jdom.Element;

/**
 * Writes to files that contain snapshotinfos.  A record consists of a snapshot info
 * represented as XML.
 * 
 */
public class SnapshotLogManager {
  private SortedListMap<Long,SnapshotLogFile> logFiles = new SortedListMap<Long,SnapshotLogFile>();
  private int numHeadersOnHand;
  private int maxSize;
  private LogDirectory logDirectory;
  private ReentrantLock writeLock = new ReentrantLock();
  
  /**
   * 
   * @param maxSize The maximum size of each file in bytes
   * @param numHeadersOnHand The number of headers to keep in ram at a given time
   * @param logDirectory LogDirectory to use for the snapshotlog
   * @throws IOException
   */
  public SnapshotLogManager(int maxSize, int numHeadersOnHand, LogDirectory logDirectory) throws IOException {
    if (numHeadersOnHand > maxSize) throw new IOException("maxSize must be greater than numHeadersOnHand");
    this.maxSize = maxSize;
    this.numHeadersOnHand = numHeadersOnHand;
    this.logDirectory = logDirectory;
    for (String name : logDirectory.list()) {
      if (name.startsWith("snapshot")) {
        Long id = getIdFromName(name);
        SnapshotLogFile snapshotLogFile = new SnapshotLogFile(id, name, logDirectory, numHeadersOnHand);
        logFiles.put(id, snapshotLogFile);
      }
    }
    if (logFiles.size() == 0) {
      createNewLogFile();
    }
  }
  
  /**
   * Close the underlying log files
   * @throws IOException
   */
  public void close() throws IOException {
    for (SnapshotLogFile logFile : logFiles.values()) {
      logFile.close();
    }
  }
  
  /**
   * Number of log files
   * @return
   */
  public int numLogFiles() {
    return logFiles.size();
  }
  
  /**
   * Format the id into a log file name
   * @param id
   * @return
   */
  public static String getNameFromId(Long id) {
    DecimalFormat decimalFormat = (DecimalFormat) DecimalFormat.getInstance();
    decimalFormat.applyPattern("00000000");
    return "snapshot" + decimalFormat.format(id) + ".log";
  }
  
  /**
   * Get the id from the file name
   * @param string
   * @return
   */
  public static Long getIdFromName(String string) {
    int dot = string.lastIndexOf('.');
    String substring = string.substring("snapshot".length(), dot);
    return new Long(substring);
  }
  
  /**
   * Get the InfoHeaders
   * @param n
   * @return
   * @throws IOException
   */
  public List<InfoHeader> getLastInfos(int n) throws IOException {
    if (n > numHeadersOnHand)
      throw new IOException("n greater than " + numHeadersOnHand);
    List<InfoHeader> infos = getCurrent().getLastInfos(n);
    List<InfoHeader> returnInfos = new ArrayList<InfoHeader>(infos);
    // add more from previous log file if there is one
    if (n > infos.size() && logFiles.size() > 1) {
      List<InfoHeader> otherInfos = logFiles.get(logFiles.size() - 2).getLastInfos(n - infos.size());
      returnInfos.addAll(otherInfos);
    }
    return returnInfos;
  }
  
  /**
   * Get the log file for the id
   * @param id
   * @return
   */
  public SnapshotLogFile getLogFile(Long id) {
    return logFiles.get(id);
  }
  
  /**
   * Get the current log file
   * @return
   */
  private SnapshotLogFile getCurrent() {
    return logFiles.lastValue();
  }

  private void deleteOldLogFile() throws IOException {
    if (logFiles.size() > 1) {
      SnapshotLogFile current = logFiles.lastValue();
      List<InfoHeader> currentHeaders = current.getLastInfos(numHeadersOnHand);
      // if current log file has enough headers
      if (currentHeaders.size() >= numHeadersOnHand) {
        // can delete old files
        List<Long> deleteIds = new ArrayList<Long>();
        for (int x = 0; x < logFiles.size() - 1; x++) {
          SnapshotLogFile logFile = logFiles.get(x);
          logFile.delete();
          deleteIds.add(logFile.getId());
        }
        for (Long id : deleteIds) {
          logFiles.remove(id);
        }
      }
    }
  }

  private void createNewLogFile() throws IOException {
    Long id = null;
    if (logFiles.size() == 0) {
      id = new Long(1);
    } else {
      id = logFiles.lastKey().longValue() + 1;
    }
    String name = getNameFromId(id);
    SnapshotLogFile snapshotLogFile = new SnapshotLogFile(id, name, logDirectory, numHeadersOnHand);
    logFiles.put(id, snapshotLogFile);
  }
  
  /**
   * Write the given CElement to the current log file
   * @param object
   * @return
   * @throws Exception
   */
  public InfoHeader write(CElement object) throws Exception {
    Element element = object.toElement();
    String xml = XMLUtil.outputElementOmitDeclaration(element);
    return write(xml);
  }
  
  /**
   * Write the string to the log file
   * @param string
   * @return
   * @throws IOException
   */
  public InfoHeader write(String string) throws IOException {
    writeLock.lock();
    try {
      SnapshotLogFile snapshotLogFile = getCurrent();
      InfoHeader infoHeader = snapshotLogFile.write(string);
      deleteOldLogFile();
      if (snapshotLogFile.getSize() >= maxSize) {
        createNewLogFile();
      }
      return infoHeader;
    } finally {
      writeLock.unlock();
    }
  }
  
  /**
   * Load the max snapshot info
   * @return
   * @throws Exception
   */
  public SnapshotInfo loadMaxSnapshotInfo() throws Exception {
    List<InfoHeader> infos = getLastInfos(5);
    if (infos.size() == 0) return null;
    String xml = loadData(infos.get(0));
    Element element = XMLUtil.parseElement(xml);
    return new SnapshotInfo(element);
  }
  
  /**
   * For the given infoheader load the string associated with it
   * @param infoHeader
   * @return
   * @throws IOException
   */
  public String loadData(InfoHeader infoHeader) throws IOException {
    SnapshotLogFile snapshotLogFile = getLogFile(infoHeader.logId);
    return snapshotLogFile.loadData(infoHeader);
  }
}
